package com.gitee.wsl.doc.prosemirror.model.content

import com.gitee.wsl.doc.prosemirror.exception.RangeError
import com.gitee.wsl.doc.prosemirror.model.expr.parseExpr
import com.gitee.wsl.doc.prosemirror.model.fragment.Fragment
import com.gitee.wsl.doc.prosemirror.model.node.Node
import com.gitee.wsl.doc.prosemirror.model.node.NodeType

data class MatchEdge(val type: NodeType, val next: ContentMatch)


/** Instances of this class represent a match state of a node type's
 * [content expression](#model.NodeSpec.content), and can be used to
 * find out whether further content matches here, and whether a given
 * position is a valid end of the node.
 */
class ContentMatch internal constructor(
    // True when this match state represents a valid end of the node.
    val validEnd: Boolean
) {
    internal val next: MutableList<MatchEdge> = mutableListOf()
    internal val wrapCache: MutableList<Pair<NodeType, List<NodeType>?>> = mutableListOf()

    internal val inlineContent: Boolean
        get() = this.next.firstOrNull()?.type?.isInline == true

    // Get the first matching node type at this match position that can be generated.
    val defaultType: NodeType?
        get() {
            return next.firstOrNull { match ->
                val type = match.type
                !(type.isText || type.hasRequiredAttrs())
            }?.type
        }

    // Match a node type, returning a match after that node if successful.
    fun matchType(type: NodeType): ContentMatch? {
        return next.firstOrNull {
            it.type == type
        }?.next
    }

    // Try to match a fragment. Returns the resulting match when successful.
    fun matchFragment(frag: Fragment, start: Int = 0, end: Int = frag.childCount): ContentMatch? {
        var cur: ContentMatch? = this
        for (i in start until end) {
            cur = cur?.matchType(frag.child(i).type)
            if (cur == null) break
        }
        return cur
    }

    internal fun compatible(other: ContentMatch): Boolean {
        this.next.forEach { thisMatch ->
            other.next.forEach { otherMatch ->
                if (thisMatch.type == otherMatch.type) return true
            }
        }
        return false
    }

    // Try to match the given fragment, and if that fails, see if it can be made to match by
    // inserting nodes in front of it. When successful, return a fragment of inserted nodes (which
    // may be empty if nothing had to be inserted). When `toEnd` is true, only return a fragment if
    // the resulting match goes to the end of the content expression.
    fun fillBefore(after: Fragment, toEnd: Boolean = false, startIndex: Int = 0): Fragment? {
        val seen = mutableListOf(this)
        var search: ((match: ContentMatch, types: List<NodeType>) -> Fragment?)? = null
        search = fun(match: ContentMatch, types: List<NodeType>): Fragment? {
            val finished = match.matchFragment(after, startIndex)
            if (finished != null && (!toEnd || finished.validEnd)) {
                return Fragment.Companion.from(types.mapNotNull { tp -> tp.createAndFill(null, null as Node?, null) })
            }

            for (nextMatch in match.next) {
                val type = nextMatch.type
                val next = nextMatch.next
                if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) {
                    seen.add(next)
                    val found = search?.invoke(next, types + type)
                    if (found != null) return found
                }
            }
            return null
        }

        return search(this, emptyList())
    }

    // Find a set of wrapping node types that would allow a node of the given type to appear at this
    // position. The result may be empty (when it fits directly) and will be null when no such
    // wrapping exists.
    fun findWrapping(target: NodeType): List<NodeType>? {
        for (pair in wrapCache) {
            if (pair.first == target) {
                return pair.second
            }
        }
        val computed = this.computeWrapping(target)
        wrapCache.add(target to computed)
        return computed
    }

    @Suppress("ComplexCondition")
    internal fun computeWrapping(target: NodeType): List<NodeType>? {
        data class Active(val match: ContentMatch, val type: NodeType?, val via: Active?)

        val seen = mutableMapOf<String, Boolean>()
        val active = mutableListOf(Active(this, null, null))
        while (active.isNotEmpty()) {
            val current = active.removeFirst()
            val match = current.match
            if (match.matchType(target) != null) {
                val result = mutableListOf<NodeType>()
                var obj: Active = current
                while (obj.type != null) {
                    result.add(obj.type)
                    obj = obj.via!!
                }
                result.reverse()
                return result.toList()
            }
            for (matchEdge in match.next) {
                val type = matchEdge.type
                val next = matchEdge.next
                if (
                    !type.isLeaf &&
                    !type.hasRequiredAttrs() &&
                    type.name !in seen &&
                    (current.type == null || next.validEnd)
                ) {
                    active.add(Active(match = type.contentMatch, type, via = current))
                    seen[type.name] = true
                }
            }
        }
        return null
    }

    // The number of outgoing edges this node has in the finite automaton that describes the content
    // expression.
    val edgeCount: Int
        get() = this.next.size

    // Get the _n_th outgoing edge from this node in the finite automaton that describes the content
    // expression.
    fun edge(n: Int): MatchEdge {
        if (n >= this.next.size) throw RangeError("There's no ${n}th edge in this content match")
        return this.next[n]
    }

    override fun toString(): String {
        val seen = mutableListOf<ContentMatch>()
        var scan: ((ContentMatch) -> Unit)? = null
        scan = fun(m: ContentMatch) {
            seen.add(m)
            m.next.forEach { edge ->
                if (!seen.contains(edge.next)) scan?.invoke(edge.next)
            }
        }
        scan(this)
        return seen.mapIndexed { i, m ->
            var out = i.toString() + (if (m.validEnd) "*" else " ") + " "
            m.next.forEachIndexed { index, matchEdge ->
                out += (if (index != 0) ", " else "") + matchEdge.type.name + "->" + seen.indexOf(matchEdge.next)
            }
            return out
        }.joinToString("\n")
    }

    companion object {
        val empty = ContentMatch(true)

        internal fun parse(string: String, nodeTypes: Map<String, NodeType>): ContentMatch {
            val stream = TokenStream(string, nodeTypes)
            if (stream.next.isNullOrEmpty()) return empty
            val expr = stream.parseExpr()
            if (!stream.next.isNullOrEmpty()) stream.err("Unexpected trailing text")
            val match = dfa(nfa(expr))
            match.checkForDeadEnds(stream)
            return match
        }
    }
}